package com.fujimana.rockincat.restree.filter.ext;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.fujimana.rockincat.restree.converter.Converter;
import com.fujimana.rockincat.restree.exception.ErrorObject;
import com.fujimana.rockincat.restree.exception.HttpException;
import com.fujimana.rockincat.restree.init.Init;
import com.fujimana.rockincat.restree.init.InitializedException;
import com.fujimana.rockincat.restree.service.Response;
import com.fujimana.rockincat.restree.util.IOUtils;
import com.fujimana.rockincat.restree.util.HttpUtils.HttpStatusCode;

@com.fujimana.rockincat.restree.annotation.filter.Filter
public class ExceptionFilter implements Filter {

	private static final String EXCEPTION_XML = "/WEB-INF/exception.xml";

	private Init init;

	private Map<String, ErrorInfo> managedExceptions;

	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		ErrorObject errorObject = null;
		boolean isError = false;

		try {
			chain.doFilter(request, response);
		} catch (HttpException e) {
			isError = true;

			errorObject = new ErrorObject();

			errorObject.setMessage(e.getMessage());
			errorObject.setStatus(e.getCode());
		} catch (Exception e) {
			isError = true;

			String exceptionName = e.getClass().getName();
			errorObject = new ErrorObject();

			if (managedExceptions.containsKey(exceptionName) == true) {
				errorObject.setMessage(getMessage(e, managedExceptions
						.get(exceptionName)));
				errorObject.setStatus(managedExceptions.get(exceptionName)
						.getStatus());
			} else {
				errorObject.setMessage(e.getMessage());
				errorObject.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR);
			}

		}

		if (isError == false) {
			return;
		}
		
		((HttpServletResponse)response).setStatus(errorObject.getStatus());

		String contentType = Response.getResponse().getContentType();
		String charaset = Response.getResponse().getCharset();
		Locale locale = Response.getResponse().getLocale();

		Converter converter = getConverter(contentType);

		byte[] b = converter.format(errorObject, locale, charaset);

		if (b == null) {
			// TODO
			return;
		}

		try {
			response.getOutputStream().write(b);
			response.getOutputStream().flush();
		} finally {
			IOUtils.close(response.getOutputStream());
		}

	}

	private Converter getConverter(String contentType) {
		if (contentType == null || contentType.trim().equals("")) {
			contentType = init.getDefaultResponseContentType();
		}

		Converter c = init.getResponseConverter(contentType);

		if (c == null) {
			c = init.getResponseConverter(init.getDefaultResponseContentType());
		}

		return c;
	}

	@Override
	public void init(FilterConfig config) throws ServletException {
		this.managedExceptions = new HashMap<String, ErrorInfo>();

		this.init = (Init) config.getServletContext().getAttribute(
				Init.class.getName());

		InputStream is = config.getServletContext().getResourceAsStream(
				EXCEPTION_XML);

		if (is == null) {
			return;
		}

		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory
					.newInstance();
			factory.setNamespaceAware(false);
			DocumentBuilder builder = factory.newDocumentBuilder();

			Document document = builder.parse(is);

			XPath path = XPathFactory.newInstance().newXPath();
			NodeList list = (NodeList) path.evaluate("/exceptions/exception",
					document, XPathConstants.NODESET);

			for (int i = 0; i < list.getLength(); i++) {
				Node n = list.item(i);

				addmanagedException((String) path.evaluate("class", n,
						XPathConstants.STRING), (String) path.evaluate(
						"message", n, XPathConstants.STRING), (String) path
						.evaluate("status", n, XPathConstants.STRING));

			}

		} catch (ParserConfigurationException e) {
			throw new InitializedException(e);
		} catch (SAXException e) {
			throw new InitializedException(e);
		} catch (IOException e) {
			throw new InitializedException(e);
		} catch (XPathExpressionException e) {
			throw new InitializedException(e);
		}

	}

	private void addmanagedException(String clazz, String message, String status) {

		try {
			ErrorInfo errorInfo = new ErrorInfo();

			Class.forName(clazz);

			if (message != null && message.trim().equals("") == false) {
				errorInfo.setMessage(message);
			}

			if (status != null && status.trim().equals("") == false) {
				errorInfo.setStatus(Integer.parseInt(status));
			}

			managedExceptions.put(clazz, errorInfo);

		} catch (ClassNotFoundException e) {
			throw new InitializedException(e);
		} catch (NumberFormatException e) {
			throw new InitializedException(e);
		}

	}

	private String getMessage(Exception e, ErrorInfo info) {
		if (info.getMessage() != null) {
			return info.getMessage();
		}

		return e.getMessage();
	}

	class ErrorInfo {
		private String message;

		private int status = HttpStatusCode.INTERNAL_SERVER_ERROR;

		public String getMessage() {
			return message;
		}

		public void setMessage(String message) {
			this.message = message;
		}

		public int getStatus() {
			return status;
		}

		public void setStatus(int status) {
			this.status = status;
		}

	}
}
